Skip to content

fix(memory): cap the memory journal so it keeps its newest changes - #2809

Open
Birfy wants to merge 1 commit into
mainfrom
dev/2807-memory-journal-cap
Open

Birfy wants to merge 1 commit into
mainfrom
dev/2807-memory-journal-cap

Conversation

@Birfy

@Birfy Birfy commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

fix(memory): cap the memory journal so it keeps its newest changes

Fixes #2807

Owner: Dev
Reviewer: Birfy
Written-by: Dev
Bot: Dev
Risk tier: T2
Base: f42ad01
Claim-Id: 3f049f868ab3ff391e0301d6dc1aea19

What changed

<home>/memory/journal.jsonl was append-only and never pruned: every remember, replace, forget and archive added a line, and nothing ever removed one. The memory itself is at a steady state, so only this side channel grew without bound.

core/src/host/memory.js now keeps the newest changes under two caps, JOURNAL_KEPT (500 lines) and JOURNAL_MAX_BYTES (1 MiB), the same shape as the state journal's pruneEntries: past a cap the oldest lines are folded into one {"time":…,"op":"truncated","dropped":N} record, an earlier fold's count carries into the next one, and the whole file is rewritten through a temp name and rename (replacePrivateFile, the target's mode kept) so a reader never sees half a line. A trim keeps 90% of the cap, so the file is rewritten every few changes rather than on every one. The normal log lines and mode: PRIVATE_FILE_MODE are unchanged; journal.jsonl still has no reader outside the tests.

core/test/memory-journal-cap.test.js is new: a real apply() host runs 300 remember+forget turns (600 writes, MEMORY.md back to its 20-byte skeleton), then asserts the file is at most 1 MiB and 500 lines, that the oldest note is gone and one truncated record names the fold, and that a change made after the prune is still the newest line. The regenerated patches/ds-bot/ mirrors are in this diff; changelog.d/2807.fixed.md records the user-visible change.

Red first

At base f42ad016, with only the new test applied and memory.js as it was:

$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/memory-journal-cap.test.js
not ok 1 - the memory journal keeps its newest lines, like the state journal
  error: 'the journal keeps at most 500 lines (it keeps 600)'
# tests 1 / # pass 0 / # fail 1

600 lines is the defect: the 300 add+forget cycles left every line behind while the memory was empty again.

Ran at this tip

$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/memory-journal-cap.test.js
# tests 1  # pass 1  # fail 0
$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/memory*.test.js
# tests 134  # pass 134  # fail 0
$ TZ=UTC npm run verify            # repo root, exit 0
core: # tests 3198 / # pass 3197 / # fail 0 / # skipped 1
social 619/619, tree 145/145, timeline 161/161, workflow 528/528
.github/scripts + policy 673/673, interop 37/37, live-e2e 34/34
core lint: Found 88 warnings and 0 errors; arch-docs: OK (5 changed file(s))
build-client --check green; 10 patches apply cleanly; core/ equals ds-bot/ + patches/ds-bot/
canonical: patches/ds-bot/ is what generate writes (generate is a no-op)
$ node .github/scripts/changelog.mjs check
changelog: 25 fragment(s) in changelog.d/ are well formed

Not run

  • The smoke test: not part of npm run verify; it needs the test bench with a harness checkout, and this change is covered by the core suite above.
  • npm run pack, the typecheck and actionlint: the CI package job runs them; they are not part of npm run verify.

Docs-not-needed: the retention bound sits inside the core-host page's existing description of the memory journal (file checks, write path, side channel); it adds no folder, no API and no new reader.

Checkpoint

{
  "pr": 2809,
  "claim": "Dev",
  "issue": 2807,
  "summary": "The memory journal was append-only: 300 remember+forget turns left 600 lines while MEMORY.md was back to 20 bytes. `memory.js` now keeps the newest 500 changes (1 MiB) and folds the rest into one `truncated` record, rewritten through a temp name like the state journal. A new real-host test reproduces the growth and pins the cap, the fold count and the newest-line rule; regenerated `patches/ds-bot/` mirrors and a changelog fragment ride along.",
  "files": [
    "core/src/host/memory.js",
    "core/test/memory-journal-cap.test.js",
    "patches/ds-bot/0001-core-fixes.patch",
    "patches/ds-bot/files/test/memory-journal-cap.test.js",
    "changelog.d/2807.fixed.md"
  ],
  "checks": {
    "ran": "TZ=UTC node --import ./test/harness-resolve.mjs --test test/memory-journal-cap.test.js (1/1); test/memory*.test.js (134/134); TZ=UTC npm run verify (core 3197/3198, social 619, tree 145, timeline 161, workflow 528, scripts+policy 673, interop 37, live-e2e 34; arch-docs OK; canonical no-op)",
    "result": "green",
    "at": "bf6ccf943e3024c3b9e7adb89f72df2cc262a8f7"
  },
  "next": "none — ready for review"
}

The memory journal under <home>/memory/journal.jsonl was append-only and never
pruned: 300 remember+forget turns left 600 lines behind while MEMORY.md was
back to its empty skeleton. It now keeps the newest 500 changes and at most
1 MiB, folding everything older into one `truncated` record and rewriting the
file through a temp name.

Fixes #2807

Docs-not-needed: the retention bound sits inside the core-host page's existing
description of the memory journal (file checks, write path, side channel); it
adds no folder, no API and no new reader.
@Birfy Birfy added the agent-pr Pull request opened by an agent label Nov 10, 2026
@Birfy Birfy self-assigned this Nov 10, 2026
@github-actions github-actions Bot added the risk:T2 Risk tier T2 (host/plugin code or an unnamed path), set by the controller from the diff label Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-pr Pull request opened by an agent risk:T2 Risk tier T2 (host/plugin code or an unnamed path), set by the controller from the diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memory journal (~/.dsh/memory/journal.jsonl) is append-only and never pruned, while memory itself stays at a steady state

1 participant